C语言深度解剖二 —— 符号

本节主要来讲讲C语言中的符号

符号 名称 符号 名称
, 逗号 > 右尖括号
. 圆点 ! 感叹号
; 分好 l 竖线
: 冒号 / 斜杠
? 问号 \ 反斜杠
单引号 ~ 波折号
双引号 # 井号
( 左圆括号 ) 右圆括号
[ 左方括号 ] 右方括号
{ 左大括号 } 又大括号
% 百分号 & and(与)
^ xor(异或) * 乘号
- 减号 = 等于号
< 左尖括号 + 加号

“国际 C 语言乱码大赛( IOCCC)”,下面这个例子就是网上广为流传的一个经典作品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#i nclude <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_,
main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m.vpbks,fxntdCeghiry"),a+1);}

2.1 注释符号

2.1.1 几个似是而非的注释问题

C 语言的注释可以出现在 C 语言代码的任何地方。这句话对不对?

1
2
3
4
5
A) int/*...*/i;
B) char* s = "abcdefg //hijklmn";
C) //Is it a \
valid comment?
D) in/*...*/t i;

我们知道 C 语言里可以有两种注释方式: / / 和//。上述前3条注释都是正确的,最后一条不正确。

A),有人认为编译器剔除掉注释后代码会被解析成 inti,所以不正确。编译器的确会将注释剔除,但不是简单的剔除,而是用空格代替原来的注释。

B),我们知道双引号引起来的都是字符串常量,那双斜杠也不例外。

C),这是一条合法的注释,因为\是一个接续符。

D), 前面说过注释会被空格替换,那这条注释不正确就很好理解了。

但注意: /*…*/这种形式的注释不能嵌套,如:/*这是/*非法的*/*/因为/*总是与离它最近的*/匹配

2.1.2 y = x/*p

y = x/*p,这是表示 x 除以 p 指向的内存里的值,把结果赋值为 y?我们可以在编译器上测试一下,编译器提示出错。
实际上,编译器把/*当作是一段注释的开始,把/*后面的内容都当作注释内容,直到出现*/为止。这个表达式其实只是表示把 x 的值赋给 y, /*后面的内容都当作注释。但是,由于没有找到*/,所以提示出错。

我们可以把上面的表达式修改一下:

1
2
3
y = x/ *p
或者
y = x/(*p)

这样的话,表达式的意思就是 x 除以 p 指向的内存里的值,把结果赋值为 y 了.也就是说只要斜杠( /)和星号( *)之间没有空格,都会被当作注释的开始。这一点一定要注意。

2.1.3 怎样才能写出出色的注释

注释写的出色非常不易,但是写的糟糕却是人人可为之。糟糕的注释只会帮倒忙。

  • 注释应当准确、易懂,防止有二义性。错误的注释不但无益反而有害。
  • 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要及时删除。
  • 注释是对代码的“提示”,而不是文档。程序中的注释应当简单明了,注释太多了会让人眼花缭乱
  • 一目了然的语句不加注释
  • 对于全局数据(全局变量、常量定义等)必须要加注释。
  • 注释采用英文,尽量避免在注释中使用缩写,特别是不常用缩写。
  • 注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但不可放在下方。同一结构中不同域的注释要对齐。
  • 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
  • 注释的缩进要与代码的缩进一致。
  • 注释代码段时应注重“为何做( why)”,而不是“怎么做( how)”。
  • 数值的单位一定要注释。
  • 对变量的范围给出注释。
  • 对一系列的数字编号给出注释,尤其在编写底层驱动程序的时候(比如管脚编号)。
  • 对于函数的入口出口数据给出注释。

2.2接续符和转移符

C 语言里以反斜杠( \)表示断行。编译器会将反斜杠剔除掉,跟在反斜杠后面的字符自动接续到前一行。但是注意:反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。

反斜杠除了可以被用作接续符,还能被用作转义字符的开始标识。

常用的转义字符及其含义:

转义字符 转义字符的意义
\n 回车换行
\t 横向跳到下一制表位置
\v 竖向跳格
\b 退格
\r 回车
\f 走纸换页
\ 反斜扛符”\”
\’ 单引号符
\a 鸣铃
\ddd 1~3 位八进制数所代表的字符
\xhh 1~2 位十六进制数所代表的字符

广义地讲, C 语言字符集中的任何一个字符均可用转义字符来表示。表中的\ddd\xhh正是为此而提出的。 ddd 和 hh 分别为八进制和十六进制的 ASCII 代码。如\102 表示字母”B”,\134 表示反斜线, \X0A 表示换行等。

2.3 单引号、双引号

我们知道双引号引起来的都是字符串常量,单引号引起来的都是字符常量。但初学者还是容易弄错这两点。比如:‘ a’和“ a”完全不一样,在内存里前者占 1 个 byte,后者占2个 byte。

字符在内存里是以 ASCAII 码存储的,所以字符常量可以与整形常量或变量进行运算。如:‘ A‘+ 1。

2.4 逻辑运算符

||和&&是我们经常用到的逻辑运算符,与按位运算符|和&是两码事。虽然简单,但毕竟容易犯错。看例子:

1
2
3
4
5
6
int i = 0;
int j = 0;
if((++i > 0) || (++j) > 0) {
//打印出i和j的值
}
结果:i = 1,j = 0;

if((++i>0)||(++j>0))语句中,先计算(++i>0),发现其结果为真,后面
的(++j>0)便不再计算。同样&&运算符也要注意这种情况。这是很容易出错的地方。

2.5 位运算符

C语言中位运算包括下面几种:

1
2
3
4
5
6
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
>> 右移

前 4 种操作很简单,一般不会出错。但要注意按位运算符|和&与逻辑运算符||和&&完全是两码事,别混淆了。其中按位异或操作可以实现不用第三个临时变量交换两个变量的值:a ^= b; b ^= a;a ^= b;但并不推荐这么做,因为这样的代码读起来很费劲。

2.5.1 左移和右移

左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数,高位丢弃,低位补 0。

右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位, “>>”右边的数指定移动的位数。

但注意:对于有符号数,在右移时,符号位将随同移动。当为正数时, 最高位补 0;而为负数时,符号位为 1,最高位是补 0 或是补 1 取决于编译系统的规定。 Turbo C 和很多系统规定为补 1。

2.5.2 0x01 << 2 + 3

再看看下面的例子:

1
0x01 << 2 + 3

结果为7吗?测试一下。结果为32?别惊讶,32才是正确答案。因为“+”号的优先级比移位运算符的优先级高。在32位系统下,改写这个例子:

1
2
3
0x01 << 2 + 30;
0x01 < 2 - 3;

这样行吗?不行,一个整型数长度为32位,左移32位发生什么事情?溢出!左移-1位呢?反过来移位?所以,移位还是有讲究的。左移和右移的位数不能大于数据的长度,不能小于0。

2.6 花括号

2.7 ++、–操作符

先看代码:

1
2
int i = 3;
(++i) + (++i) + (++i);

表达式的值为多少? 15 吗? 16 吗? 18 吗?其实对于这种情况, C语言标准并没有作出规定。有点编译器计算出来为 18,因为 i 经过 3 次自加后变为 6,然后 3 个 6 相加得 18;而有的编译器计算出来为 16(比如 Visual C++6.0),先计算前两个 i 的和,这时候 i 自加两次, 2 个 i 的和为 10,然后再加上第三次自加的 i 得 16。但不会计算出 15 的结果来的。

++、 –作为前缀,我们知道是先自加或自减,然后再做别的运算;但是作为后缀时,到底什么时候自加、自减?假设 i=0,看例子:

1
2
3
4
5
A) j = (i++, i++, i++);
B) for(i = 0; i < 10; i++) {
//code
}
C) k = (i++) + (i++) + (i++);

A)例子为逗号表达式,i在遇到每个逗号后,认为本计算单位已经结束,i这时候自加。关于逗号表达式与“++”或“–”的连用,还有一个比较好的例子:

1
2
3
int x;
int i = 3;
x = (++i, i++, i + 10);

问x的值是多少?i的值为多少?
按照上面的讲解,可以很清楚的知道,逗号表达式中,i在遇到每个逗号后,认为本计算单位已经结束,i这时候自加。所以,本例子计算完后,i的值为5,x的值为15。

B) 例子 i 与 10 进行比较之后,认为本计算单位已经结束, i 这时候自加。

C) 例子 i 遇到分号才认为本计算单位已经结束, i 这时候才自加(k结果是0)。也就是说后缀运算是在本计算单位计算结束之后再自加或自减。 C 语言里的计算单位大体分为以上 3 类。

2.7.1 ++i+++i+++i

上面的例子很简单,那我们把括号去掉看看:

1
2
int i = 3;
++i+++i+++i;

好,我们先看看这个a+++b和下面哪个表达式相当:

1
2
A) a++ +b;
B) a+ ++b;

2.7.2 贪心法

C语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理的策略被称为“贪心法”。需要注意的是,除了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行等)。比如:==是单个符号,而= =是两个等号。

根据上面的规则可以很轻松的判断a+++b表达式与a++ +b一致。那么++i+++i+++i;则会被分解成++i + ++i + ++i;a+++++b将会被分解成a++ + ++b

2.8 (-3)/2的值是多少?

3/2的值是多少?(-3)%2呢?

C语言中的取整法则很简单,当商大于0时向下取整,当商小于0时向上取整,就是近0法则。C语言中的取余数法则也很简单,余数的符号总是与被除数的符号相同。记住这两条规则就够了。

通过上面的规则我们就知道了,3/2的结果是1,(-3)/2的结果是-1,3%2的结果是1,(-3)%2的结果是-1。3/(-2)的结果是-1,3%(-2)的结果是1。

2.9 运算符的优先级

2.9.1 运算符的优先级表

下表示C语言运算符的优先级表:

优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下表 数组名[常量表达式] 左到右
1 () 圆括号 (表达式)/函数名(形参表) 左到右
1 . 成员选择(对象) 对象.成员 左到右
1 -> 成员选择(指针) 对象指针-> 成员名 左到右
2 - 负号运算符 -表达式 右到左 单目运算符
2 (类型) 强制类型转换 (数据类型)表达式 右到左
2 ++ 自增运算符 ++变量名/变量名++ 右到左 单目运算符
2 自减员算符 –变量名/变量名– 右到左 单目运算符
2 * 取值运算符 *指针变量 右到左 单目运算符
2 & 取地址运算符 &变量名 右到左 单目运算符
2 逻辑非运算符 !表达式 右到左 单目运算符
2 ~ 按位取反运算符 ~表达式 右到左 单目运算符
2 sizeof 长度运算符 sizeof(表达式) 右到左 单目运算符
3 / 表达式/表达式 左到右 双目运算符
3 * 表达式*表达式 左到右 双目运算符
3 % 取余 整型表达式/整型表达式 左到右 双目运算符
4 + 表达式+表达式 左到右 双目运算符
4 - 表达式-表达式 左到右 双目运算符
5 << 左移 变量<<表达式 左到右 双目运算符
5 >> 右移 变量>>表达式 左到右 双目运算符
6 > 大于 表达式>表达式 左到右 双目运算符
6 >= 大于等于 表达式>=表达式 左到右 双目运算符
6 < 小于 表达式<表达式 左到右 双目运算符
6 <= 小于等于 表达式<=表达式 左到右 双目运算符
7 == 等于 表达式==表达式 左到右 双目运算符
7 != 不等于 表达式!=表达式 左到右 双目运算符
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 l 按位或 表达式l表达式 左到右 双目运算符
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 ll 逻辑或 表达式ll表达式 左到右 双目运算符
13 ?: 条件运算符 表达式1 ? 表达式2 : 表达式3 左到右 三目运算符
14 = 赋值运算符 变量=表达式 右到左
14 /= 除后赋值 变量/=表达式 右到左
14 *= 乘后赋值 变量*=表达式 右到左
14 %= 取余后赋值 变量%=表达式 右到左
14 += 加后赋值 变量+=表达式 右到左
14 -= 减后赋值 变量==表达式 右到左
14 <<= 左移后赋值 变量<<=表达式 右到左
14 >>= 右移后赋值 变量>>=表达式 右到左
14 &= 按位与或后赋值 变量&=表达式 右到左
14 ^= 按位异或后赋值 变量^=表达式 右到左
14 l= 按位或后赋值 变量l=表达式 右到左
15 , 逗号表达式 表达式,表达式,表达式 左到右 从左到右顺序运算

注:同一优先级的运算符,运算次序由结合方向所决定。

2.9.2 一些容易出错的优先级问题

下表中整理了容易出错的情况:

优先级问题 表达式 经常误认为的结果 实际结果
.的优先级高于* *p.f p所指对象的字段f (*p).f 对p取f偏移,作为指针,然后进行接触引用操作。*(p.f)
[]高于* int *ap[] ap是个指向int数组的指针 ap是个元素为int的指针数组
== 和 !=高于位操作符 (val & mask != 0) (val & mask) != 0 val & (mask != 0)
== 和!= 高于赋值操作符 c = getchar() != EOF (c = getchar()) != EOF c = (getchar() != EOF )
算术运算符高于位运算符 msb << 4+lsb (msb << 4) + lsb msb << (4 + lsb)
逗号运算符在所有优先级中最低 i = 1, 2 i = (1, 2) (i = 1), 2
文章目录
  1. 1. 2.1 注释符号
    1. 1.1. 2.1.1 几个似是而非的注释问题
    2. 1.2. 2.1.2 y = x/*p
    3. 1.3. 2.1.3 怎样才能写出出色的注释
  2. 2. 2.2接续符和转移符
  3. 3. 2.3 单引号、双引号
  4. 4. 2.4 逻辑运算符
  5. 5. 2.5 位运算符
    1. 5.1. 2.5.1 左移和右移
    2. 5.2. 2.5.2 0x01 << 2 + 3
  6. 6. 2.6 花括号
  7. 7. 2.7 ++、–操作符
    1. 7.1. 2.7.1 ++i+++i+++i
    2. 7.2. 2.7.2 贪心法
  8. 8. 2.8 (-3)/2的值是多少?
  9. 9. 2.9 运算符的优先级
    1. 9.1. 2.9.1 运算符的优先级表
    2. 9.2. 2.9.2 一些容易出错的优先级问题
,